/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*- */
/*
 *  Copyright 2008 Global Sign In
 * 
 *  This code is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; either
 *  version 2 of the License, or (at your option) any later version.
 *
 *  This library is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *  Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */

#include <time.h>
#include <ctype.h>
#include <stdlib.h>
#include <stdarg.h>
#include <strings.h>
#include <sstream>
#include <iostream>
#include <algorithm>

#include "config.h"
#include "ConfigurationFile.h"
#include "SMTPSession.h"
#include "Timer.h"

#define _CAN_SPECIFY_SENDER

using std::clog;
using std::endl;
using std::map;
using std::set;
using std::string;
using std::stringstream;
using std::min;

static string appendValueAndPath(const string &value, const string &path)
{
	stringstream headerStr;

	if ((value.empty() == true) &&
		(path.empty() == true))
	{
		return "";
	}

	if (value.empty() == false)
	{
		headerStr << value;
		if (path.empty() == false)
		{
			headerStr << " ";
		}
	}
	if (path.empty() == false)
	{
		headerStr << "<" << path << ">";
	}

	return headerStr.str();
}

static void eventCallback(smtp_session_t session, int eventNum, void *pArg, ...)
{
	SMTPSession *pSession = NULL;
	string domainName, errMsg;
	va_list alist;
	int *ok = NULL;

	if (pArg != NULL)
	{
		pSession = (SMTPSession *)pArg;
		domainName = pSession->getDomainName();

		if ((pSession->getError(errMsg) == SMTP_ERR_DROPPED_CONNECTION) &&
			(eventNum != SMTP_EV_DISCONNECT))
		{
			// We were not disconnected right away, reset the error condition
			pSession->recordError(true);
		}
	}

	va_start(alist, pArg);
	switch(eventNum)
	{
		case SMTP_EV_CONNECT:
			if (pSession != NULL)
			{
				// Assume we'll get disconnected right away
				pSession->setError(SMTP_ERR_DROPPED_CONNECTION);
			}
			clog << "SMTPSession: " << domainName << " SMTP_EV_CONNECT" << endl;
			break;
		case SMTP_EV_MAILSTATUS:
			clog << "SMTPSession: " << domainName << " SMTP_EV_MAILSTATUS" << endl;
			break;
		case SMTP_EV_RCPTSTATUS:
			clog << "SMTPSession: " << domainName << " SMTP_EV_RCPTSTATUS" << endl;
			break;
#ifdef DEBUG
		case SMTP_EV_MESSAGEDATA:
			clog << "SMTPSession: " << domainName << " SMTP_EV_MESSAGEDATA" << endl;
			break;
#endif
		case SMTP_EV_MESSAGESENT:
			clog << "SMTPSession: " << domainName << " SMTP_EV_MESSAGESENT" << endl;
			break;
		case SMTP_EV_DISCONNECT:
			if ((pSession != NULL) &&
				(pSession->getError(errMsg) == SMTP_ERR_DROPPED_CONNECTION))
			{
				clog << "SMTPSession: " << domainName << " SMTP_EV_DISCONNECT right after SMTP_EV_CONNECT" << endl;
			}
			else
			{
				clog << "SMTPSession: " << domainName << " SMTP_EV_DISCONNECT" << endl;
			}
			break;
		case SMTP_EV_WEAK_CIPHER:
		{
			int bits = va_arg(alist, long);
			ok = va_arg(alist, int*);
			clog << "SMTPSession: " << domainName << " SMTP_EV_WEAK_CIPHER bits=" << bits << endl;
			break;
		}
		case SMTP_EV_STARTTLS_OK:
			clog << "SMTPSession: " << domainName << " SMTP_EV_STARTTLS_OK" << endl;
			break;
		case SMTP_EV_INVALID_PEER_CERTIFICATE:
		{
			long vfy_result = va_arg(alist, long);
			ok = va_arg(alist, int*);
			clog << "SMTPSession: " << domainName << " SMTP_EV_INVALID_PEER_CERTIFICATE vfy_result=" << vfy_result << endl;
			break;
		}
		case SMTP_EV_NO_PEER_CERTIFICATE:
		{
			ok = va_arg(alist, int*); 
			clog << "SMTPSession: " << domainName << " SMTP_EV_NO_PEER_CERTIFICATE" << endl;
			*ok = 1;
			break;
		}
		case SMTP_EV_WRONG_PEER_CERTIFICATE:
		{
			ok = va_arg(alist, int*);
			clog << "SMTPSession: " << domainName << " SMTP_EV_WRONG_PEER_CERTIFICATE" << endl;
			*ok = 1;
			break;
		}
		case SMTP_EV_NO_CLIENT_CERTIFICATE:
		{
			ok = va_arg(alist, int*); 
			clog << "SMTPSession: " << domainName << " SMTP_EV_NO_CLIENT_CERTIFICATE" << endl;
			*ok = 1;
			break;
		}
		default:
#ifdef DEBUG
			clog << "SMTPSession: unknown event " << eventNum << endl;
#endif
			break;
	}
	va_end(alist);
}

string g_attachmentLine;
string g_extraLine("\r\n");
string g_startMixed("Content-Type: multipart/mixed;\r\n\tboundary=\"----=_NextMixedPart\"\r\n\r\nThis is a message in multipart MIME format. Your mail client should not be displaying this. Consider upgrading your mail client to view this message correctly.\r\n\r\n------=_NextMixedPart\r\n");
string g_startAlt("Content-Type: multipart/alternative;\r\n\tboundary=\"----=_NextAltPart\"\r\n\r\n\r\n------=_NextAltPart\r\n");
string g_startAltNoMulti("Content-Type: multipart/alternative;\r\n\tboundary=\"----=_NextAltPart\"\r\n\r\nThis is a multi-part message in MIME format.\r\n\r\n------=_NextAltPart\r\n");
string g_startPlain("Content-Type: text/plain;\r\n\tcharset=\"UTF-8\"\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\n");
string g_altTransition("------=_NextAltPart\r\n");
string g_startHtml("Content-Type: text/html;\r\n\tcharset=\"UTF-8\"\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\n");
string g_endAlt("\r\n------=_NextAltPart--\r\n\r\n");
string g_startAttachment("------=_NextMixedPart\r\n");
string g_endMixed("\r\n------=_NextMixedPart--\r\n\r\n");

const char *messageDataCallback(void **ppBuf, int *pLen, void *pArg)
{
	if (pArg == NULL)
	{
		return NULL;
	}
	SMTPMessage *pMsg = (SMTPMessage *)pArg;

	// We don't need to malloc a buffer
	*ppBuf = NULL;
	if (pLen == NULL)
	{
		// Rewind to the beginning of the message 
		pMsg->m_stage = SMTPMessage::EXTRA_HEADERS;
		pMsg->m_currentAttachment = 0;
		pMsg->m_encodedPos = 0;
#ifdef DEBUG
		clog << "SMTPSession: rewound message" << endl;
#endif
		return NULL;
	}

	const char *pBuf = NULL;

#ifdef DEBUG
	clog << "SMTPSession: stage " << pMsg->m_stage << endl;
#endif
	// EXTRA_HEADERS, START_MIXED, START_ALT, START_PLAIN, PLAIN, END_PLAIN,
	// ALT_TRANSITION, START_HTML, HTML, END_HTML, END_ALT, START_ATTACHMENT,
	// ATTACHMENT_LINE, ATTACHMENT_LINE_END, END_MIXED, END
	switch (pMsg->m_stage)
	{
		case SMTPMessage::EXTRA_HEADERS:
			if (pMsg->m_headers.empty() == false)
			{
				*pLen = (int)pMsg->m_headers.length();
				pBuf = pMsg->m_headers.c_str();
				break;
			}
			pMsg->m_stage = SMTPMessage::START_MIXED;
		case SMTPMessage::START_MIXED:
			if (pMsg->requiresMixedParts() == true)
			{
				*pLen = (int)g_startMixed.length();
				pBuf = g_startMixed.c_str();
				break;
			}
			pMsg->m_stage = SMTPMessage::START_ALT;
		case SMTPMessage::START_ALT:
			if (pMsg->requiresAlternativeParts() == true)
			{
				if (pMsg->requiresMixedParts() == true)
				{
					*pLen = (int)g_startAlt.length();
					pBuf = g_startAlt.c_str();
				}
				else
				{
					*pLen = (int)g_startAltNoMulti.length();
					pBuf = g_startAltNoMulti.c_str();
				}
				break;
			}
			pMsg->m_stage = SMTPMessage::START_PLAIN;
		case SMTPMessage::START_PLAIN:
			if (pMsg->m_plainContent.empty() == false)
			{
				*pLen = (int)g_startPlain.length();
				pBuf = g_startPlain.c_str();
				break;
			}
			pMsg->m_stage = SMTPMessage::PLAIN;
		case SMTPMessage::PLAIN:
			// This is optional
			if (pMsg->m_plainContent.empty() == false)
			{
				*pLen = (int)pMsg->m_plainContent.length();
				pBuf = pMsg->m_plainContent.c_str();
				break;
			}
			pMsg->m_stage = SMTPMessage::END_PLAIN;
		case SMTPMessage::END_PLAIN:
			if ((pMsg->m_plainContent.empty() == false) &&
				(pMsg->m_plainContent[pMsg->m_htmlContent.length()] != '\n'))
			{
				*pLen = (int)g_extraLine.length();
				pBuf = g_extraLine.c_str();
				break;
			}
			pMsg->m_stage = SMTPMessage::ALT_TRANSITION;
		case SMTPMessage::ALT_TRANSITION:
			if (pMsg->requiresAlternativeParts() == true)
			{
				*pLen = (int)g_altTransition.length();
				pBuf = g_altTransition.c_str();
				break;
			}
			pMsg->m_stage = SMTPMessage::START_HTML;
		case SMTPMessage::START_HTML:
			if (pMsg->m_htmlContent.empty() == false)
			{
				*pLen = (int)g_startHtml.length();
				pBuf = g_startHtml.c_str();
				break;
			}
			pMsg->m_stage = SMTPMessage::HTML;
		case SMTPMessage::HTML:
			// This is optional
			if (pMsg->m_htmlContent.empty() == false)
			{
				*pLen = (int)pMsg->m_htmlContent.length();
				pBuf = pMsg->m_htmlContent.c_str();
				break;
			}
			pMsg->m_stage = SMTPMessage::END_HTML;
		case SMTPMessage::END_HTML:
			if ((pMsg->m_htmlContent.empty() == false) &&
				(pMsg->m_htmlContent[pMsg->m_htmlContent.length()] != '\n'))
			{
				*pLen = (int)g_extraLine.length();
				pBuf = g_extraLine.c_str();
				break;
			}
			pMsg->m_stage = SMTPMessage::END_ALT;
		case SMTPMessage::END_ALT:
			if (pMsg->requiresAlternativeParts() == true)
			{
				*pLen = (int)g_endAlt.length();
				pBuf = g_endAlt.c_str();
				break;
			}
			pMsg->m_stage = SMTPMessage::START_ATTACHMENT;
		case SMTPMessage::START_ATTACHMENT:
			if (pMsg->m_pDetails->getAttachmentCount() > 0)
			{
				*pLen = (int)g_startAttachment.length();
				pBuf = g_startAttachment.c_str();
				break;
			}
			pMsg->m_stage = SMTPMessage::ATTACHMENT_HEADERS;
		case SMTPMessage::ATTACHMENT_HEADERS:
			if (pMsg->m_pDetails->getAttachmentCount() > 0)
			{
				string headers(pMsg->m_pDetails->getAttachmentHeaders(pMsg->m_currentAttachment));
				if (headers.empty() == false)
				{
					*pLen = (int)headers.length();
					pBuf = headers.c_str();
#ifdef DEBUG
					clog << "SMTPSession: got headers for attachment " << pMsg->m_currentAttachment << endl;
#endif
					break;
				}
			}
			pMsg->m_stage = SMTPMessage::ATTACHMENT_LINE;
		case SMTPMessage::ATTACHMENT_LINE:
			// This is optional
			if (pMsg->m_pDetails->getAttachmentCount() > 0)
			{
				unsigned long encodedLen = 0;
				const char *pEncodedData = pMsg->getAttachmentLine(encodedLen);
				if ((pEncodedData != NULL) &&
					(encodedLen > 0))
				{
#ifdef DEBUG
					clog << "SMTPSession: got " << encodedLen << " bytes long line for attachment "
						<< pMsg->m_currentAttachment << endl;
#endif
					// FIXME: if we return the line as is, libesmtp will for some reason
					// ignore the length and go all the way to the terminating NULL
					g_attachmentLine = string(pEncodedData, (string::size_type)encodedLen);
					*pLen = (int)g_attachmentLine.length();
					pBuf = g_attachmentLine.c_str();
					break;
				}
			}
			pMsg->m_stage = SMTPMessage::ATTACHMENT_LINE_END;
		case SMTPMessage::ATTACHMENT_LINE_END:
			if (pMsg->m_pDetails->getAttachmentCount() > 0)
			{
				*pLen = (int)g_extraLine.length();
				pBuf = g_extraLine.c_str();
#ifdef DEBUG
				clog << "SMTPSession: end of line for attachment " << pMsg->m_currentAttachment << endl;
#endif
				if (pMsg->hasMoreAttachmentLines() == true)
				{
					// Jump back so that the next stage outputs the next line
					pMsg->m_stage = SMTPMessage::ATTACHMENT_HEADERS;
				}
				break;
			}
			pMsg->m_stage = SMTPMessage::END_ATTACHMENT;
		case SMTPMessage::END_ATTACHMENT:
			if (pMsg->m_pDetails->getAttachmentCount() > 0)
			{
				*pLen = (int)g_extraLine.length();
				pBuf = g_extraLine.c_str();
				++pMsg->m_currentAttachment;
				pMsg->m_encodedPos = 0;
				if (pMsg->m_currentAttachment >= pMsg->m_pDetails->getAttachmentCount())
				{
					// No more attachment
					pMsg->m_stage = SMTPMessage::NEXT_ATTACHMENT;
				}
				break;
			}
			pMsg->m_stage = SMTPMessage::NEXT_ATTACHMENT;
		case SMTPMessage::NEXT_ATTACHMENT:
			if (pMsg->m_currentAttachment < pMsg->m_pDetails->getAttachmentCount())
			{
				*pLen = (int)g_extraLine.length();
				pBuf = g_extraLine.c_str();
				// Jump back so that the next stage starts the next attachment
				pMsg->m_stage = SMTPMessage::END_ALT;
				break;
			}
			pMsg->m_stage = SMTPMessage::END_MIXED;
		case SMTPMessage::END_MIXED:
			if (pMsg->requiresMixedParts() == true)
			{
				*pLen = (int)g_endMixed.length();
				pBuf = g_endMixed.c_str();
				break;
			}
			pMsg->m_stage = SMTPMessage::END;
		case SMTPMessage::END:
		default:
#ifdef DEBUG
			clog << "SMTPSession: end of message" << endl;
#endif
			*pLen = 0;
			pBuf = NULL;
			break;
	}

	pMsg->m_stage = (SMTPMessage::Stage)((int)pMsg->m_stage + 1);

	return pBuf;
}

void recipientStatusCallback(smtp_recipient_t recipient, const char *pMailbox, void *pArg)
{
	const smtp_status_t *pStatus = smtp_recipient_status(recipient);
	if ((pMailbox != NULL) &&
		(pStatus != NULL))
	{
		if (pArg != NULL)
		{
			StatusUpdater *pUpdater = (StatusUpdater *)pArg;

			pUpdater->updateRecipientStatus(pMailbox,
				pStatus->code, pStatus->text);
		}
	}
}

void messageStatusCallback(smtp_message_t message, void *pArg)
{
	const smtp_status_t *pStatus = smtp_message_transfer_status(message);

	clog << "SMTP status for message: " << pStatus->code
		<< " (" << ((pStatus->text != NULL) ? pStatus->text : "") << ")" << endl;
	// FIXME: if pStatus->code != 250, consider all recipients failed ?

	// Check all recipients for this message
	smtp_enumerate_recipients(message, recipientStatusCallback, pArg);
}

void resetRecipientsCallback(smtp_recipient_t recipient, const char *pMailbox, void *pArg)
{
	smtp_recipient_reset_status(recipient);

	if (pMailbox != NULL)
	{
		clog << "Reset recipient " << pMailbox << endl;
	}
}

void resetMessagesCallback(smtp_message_t message, void *pArg)
{
	smtp_message_reset_status(message);
	clog << "Reset message" << endl;

	smtp_enumerate_recipients(message, resetRecipientsCallback, pArg);
}

static string getMessageData(SMTPMessage *pMsg)
{
	void *ppBuf = NULL;
	int length = 0;
	const char *pBuf = NULL;
	string fullMessage;

	// Rewind
	messageDataCallback(&ppBuf, NULL, pMsg);
	do
	{
		// Get data
		pBuf = messageDataCallback(&ppBuf, &length, pMsg);
		if (pBuf != NULL)
		{
			fullMessage += pBuf;
		}
	} while (pBuf != NULL);

	return fullMessage;
}

static bool isIPAddress(const string &hostName)
{
	// Is the domain really a domain ?
	for (string::size_type i = 0; i < hostName.length(); ++i)
	{
		if ((isdigit(hostName[i]) == 0) &&
			(hostName[i] != '.'))
		{
			return false;
		}
	}

	return true;
}

SMTPMessage::SMTPMessage(const map<string, string> &fieldValues,
	MessageDetails *pDetails, notify_flags dsnFlags,
	bool enableMdn) :
	m_pDetails(pDetails),
	m_dsnFlags(dsnFlags),
	m_enableMdn(enableMdn),
	m_stage(EXTRA_HEADERS),
	m_currentAttachment(0),
	m_encodedPos(0),
	m_hasMoreLines(false)
{
	if (m_pDetails != NULL)
	{
		// Substitute fields in subject and content
		m_pDetails->getSubjectSubstituteObj()->substitute(fieldValues, m_subject);
		m_pDetails->getPlainSubstituteObj()->substitute(fieldValues, m_plainContent);
		m_pDetails->getHtmlSubstituteObj()->substitute(fieldValues, m_htmlContent);
	}

	buildHeaders();
}

SMTPMessage::~SMTPMessage()
{
}

void SMTPMessage::buildHeaders(void)
{
	ConfigurationFile *pConfig = ConfigurationFile::getInstance("");

	appendHeader("MIME-Version", "1.0", "");
	appendHeader("User-Agent", PACKAGE_STRING, "");
	if (pConfig != NULL)
	{
		appendHeader("X-Complaints-To", pConfig->m_complaints, "");
	}

	if (m_pDetails != NULL)
	{
		if (m_subject.empty() == false)
		{
			appendHeader("Subject", m_subject, "");
		}

		if (m_pDetails->m_senderEmailAddress.empty() == false)
		{
#ifdef _CAN_SPECIFY_SENDER
			appendHeader("Sender", "", m_pDetails->m_senderEmailAddress);
#endif
			appendHeader("Resent-From", "", m_pDetails->m_senderEmailAddress);
		}

		if (pConfig != NULL)
		{
			appendHeader("Resent-Message-Id", "<" + m_pDetails->createMessageId(pConfig->m_msgIdSuffix) + ">", "");
			appendHeader("Message-Id", "<" + m_pDetails->createMessageId(pConfig->m_msgIdSuffix) + ">", "");
		}

		if (m_pDetails->m_fromEmailAddress.empty() == false)
		{
			appendHeader("From", m_pDetails->m_fromName, m_pDetails->m_fromEmailAddress);
		}

		if (m_pDetails->m_replyToEmailAddress.empty() == false)
		{
			appendHeader("Reply-To", m_pDetails->m_replyToName, m_pDetails->m_replyToEmailAddress);
		}

		struct tm timeTm;
		time_t aTime = time(NULL);
		if (localtime_r(&aTime, &timeTm) != NULL)
		{
			char dateStr[128];

			// Generate a RFC [2]822 compliant date
#if defined(__GNU_LIBRARY__)
			// %z is a GNU extension
			if (strftime(dateStr, 128, "%a, %d %b %Y %H:%M:%S %z", &timeTm) > 0)
#else
			if (strftime(dateStr, 128, "%a, %d %b %Y %H:%M:%S %Z", &timeTm) > 0)
#endif
			{
				appendHeader("Date", dateStr, "");
			}
		}
	}
}

void SMTPMessage::appendHeader(const string &header, const string &value, const string &path)
{
	stringstream headerStr;

	// We need at least header and a value or a path
	if (header.empty() == true)
	{
		return;
	}
	if ((value.empty() == true) &&
		(path.empty() == true))
	{
		return;
	}

	headerStr << header << ":";
	if (value.empty() == false)
	{
		headerStr << " " << value;
	}
	if (path.empty() == false)
	{
		headerStr << " <" << path << ">";
	}
	headerStr << "\r\n";

#ifdef DEBUG
	clog << "SMTPMessage::appendHeader: " << headerStr.str();
#endif
	m_headers.append(headerStr.str());

	// Build a colon separated list of headers
	// That will be useful when signing the message
	if (m_headersList.empty() == false)
	{
		m_headersList.append(":");
	}
	m_headersList.append(header);
}

bool SMTPMessage::requiresMixedParts(void) const
{
	if ((m_pDetails != NULL) &&
		(m_pDetails->getAttachmentCount() > 0))
	{
#ifdef DEBUG
		clog << "SMTPMessage::requiresMixedParts: yes" << endl;
#endif
		return true;
	}

	return false;
}

bool SMTPMessage::requiresAlternativeParts(void) const
{
	if ((m_plainContent.empty() == false) &&
		(m_htmlContent.empty() == false))
	{
#ifdef DEBUG
		clog << "SMTPMessage::requiresAlternativeParts: yes" << endl;
#endif
		return true;
	}

	return false;
}

string SMTPMessage::getFromEmailAddress(void) const
{
	if (m_pDetails != NULL)
	{
		return m_pDetails->m_fromEmailAddress;
	}

	return "";
}

string SMTPMessage::getSenderEmailAddress(void) const
{
	if (m_pDetails != NULL)
	{
		return m_pDetails->m_senderEmailAddress;
	}

	return "";
}

const char *SMTPMessage::getAttachmentLine(unsigned long &lineLen)
{
	unsigned long encodedLen = 0;
	const char *pEncodedContent = m_pDetails->getEncodedAttachment(m_currentAttachment, encodedLen);

	m_hasMoreLines = false;

	if ((pEncodedContent != NULL) &&
		(encodedLen > 0) &&
		(m_encodedPos < encodedLen))
	{
		const char *pLine = pEncodedContent + m_encodedPos;

		lineLen = min((unsigned long)76, encodedLen - m_encodedPos);

		// Move forward
		m_encodedPos += lineLen;
		// Any more line after this ?
		if (m_encodedPos < encodedLen)
		{
			m_hasMoreLines = true;
		}

		return pLine;
	}

	return NULL;
}

bool SMTPMessage::hasMoreAttachmentLines(void) const
{
	return m_hasMoreLines;
}

pthread_mutex_t SMTPSession::m_mutex = PTHREAD_MUTEX_INITIALIZER;

SMTPSession::SMTPSession(const DomainLimits &domainLimits,
	const string &localHostName) :
	m_domainLimits(domainLimits),
	m_msgsCount(0),
	m_session(NULL),
	m_topPriority(1000000),
	m_dontSend(false),
	m_mutexSessions(false),
	m_errorNum(-1)
{
	char *pEnvVar = getenv("GIVEMAIL_DONT_SEND");

	// This deactivates sending
	if ((pEnvVar != NULL) &&
		(strncasecmp(pEnvVar, "Y", 1) == 0))
	{
		m_dontSend = true;
	}

	pEnvVar = getenv("GIVEMAIL_MUTEX_SESSIONS");

	// This protects libesmtp sessions with a mutex
	if ((pEnvVar != NULL) &&
		(strncasecmp(pEnvVar, "Y", 1) == 0))
	{
		m_mutexSessions = true;
	}
}

SMTPSession::~SMTPSession()
{
	destroySession();
}

bool SMTPSession::createSession(void)
{
	if (m_session == NULL)
	{
		m_session = smtp_create_session();
		if (m_session == NULL)
		{
			return false;
		}

		smtp_set_eventcb(m_session, eventCallback, this);

		// Cycle to the next server
		if (cycleServers() == false)
		{
			return false;
		}
	}

	return true;
}

void SMTPSession::destroySession(void)
{
	if (m_session != NULL)
	{
		smtp_destroy_session(m_session);
		m_session = NULL;
	}
}

bool SMTPSession::initializeAddresses(ResourceRecord &mxRecord)
{
	set<ResourceRecord> aRecords;

	if (isIPAddress(mxRecord.m_hostName) == true)
	{
		ResourceRecord aRecord(mxRecord.m_domainName,
			10, mxRecord.m_hostName, 36000, time(NULL));

		aRecords.insert(aRecord);
	}
	else if (Resolver::queryARecords(mxRecord.m_hostName, aRecords) == false)
	{
		return false;
	}

	// Empty the list of A records
	while (mxRecord.m_addresses.empty() == false)
	{
		mxRecord.m_addresses.pop();
	}

	for (set<ResourceRecord>::const_iterator addressIter = aRecords.begin();
		addressIter != aRecords.end(); ++addressIter)
	{
		// Don't put discarded records back in
		if (isDiscarded(*addressIter) == false)
		{
			clog << "Host " << mxRecord.m_hostName
				<< " has IP address " << addressIter->m_hostName << endl;

			mxRecord.m_addresses.push(*addressIter);
		}
	}

	return !mxRecord.m_addresses.empty();
}

bool SMTPSession::initializeTopQueue(void)
{
	bool isIP = isIPAddress(m_domainLimits.m_domainName);

#ifdef DEBUG
	clog << "SMTPSession::initializeTopQueue: " << m_domainLimits.m_domainName << " " << isIP << endl;
#endif
	if (isIP == true)
	{
		ResourceRecord mxRecord(m_domainLimits.m_domainName,
			10, m_domainLimits.m_domainName, 36000, time(NULL));
		ResourceRecord aRecord(m_domainLimits.m_domainName,
			10, m_domainLimits.m_domainName, 36000, time(NULL));

		mxRecord.m_addresses.push(aRecord);
		m_topQueue.push(mxRecord);

		return true;
	}

	time_t timeNow = time(NULL);

	// Go through MX records
	// They are sorted in order of increasing priority
	for (set<ResourceRecord>::const_reverse_iterator serverIter = m_domainLimits.m_mxRecords.rbegin();
		serverIter != m_domainLimits.m_mxRecords.rend(); ++serverIter)
	{
		// Set priority ?
		if ((m_topPriority == 1000000) &&
			(m_topPriority > serverIter->m_priority))
		{
			m_topPriority = serverIter->m_priority;
#ifdef DEBUG
			clog << "SMTPSession::initializeTopQueue: top priority " << m_topPriority << endl;
#endif
		}

		if (serverIter->m_priority > m_topPriority)
		{
			// This record and all those that follow are lower priority
			break;
		}

		ResourceRecord mxRecord(*serverIter);
		if ((mxRecord.hasExpired(timeNow) == false) &&
			(initializeAddresses(mxRecord) == true))
		{
			m_topQueue.push(mxRecord);
		}
	}
#ifdef DEBUG
	clog << "SMTPSession::initializeTopQueue: " << m_topQueue.size()
		<< " servers of priority " << m_topPriority << endl;
#endif

	return !m_topQueue.empty();
}

bool SMTPSession::queueNextPriorityRecords(void)
{
	time_t timeNow = time(NULL);
	int currentPriority = 1000000;

	// Go through MX records
	// They are sorted in order of increasing priority
	for (set<ResourceRecord>::const_reverse_iterator serverIter = m_domainLimits.m_mxRecords.rbegin();
		serverIter != m_domainLimits.m_mxRecords.rend(); ++serverIter)
	{
		if (serverIter->m_priority <= m_topPriority)
		{
			// Keep looking for lower priority records
			continue;
		}

		// Set priority ?
		if ((currentPriority == 1000000) &&
			(currentPriority > serverIter->m_priority))
		{
			currentPriority = serverIter->m_priority;
		}

		if (serverIter->m_priority < currentPriority)
		{
			// This record and all those that follow are lower priority
			break;
		}

		if (serverIter->hasExpired(timeNow) == false)
		{
			// Push records of the same priority
			m_topQueue.push(*serverIter);
		}
	}
	m_topPriority = currentPriority;
#ifdef DEBUG
	clog << "SMTPSession::queueNextPriorityRecords: top priority " << m_topPriority << endl;
#endif

	return !m_topQueue.empty();
}

bool SMTPSession::setServer(ResourceRecord &mxRecord)
{
	if (mxRecord.m_hostName.empty() == true)
	{
		return false;
	}

	if (mxRecord.m_addresses.empty() == true)
	{
		// We have run out of A records to try...
		// Get them all again
		if (initializeAddresses(mxRecord) == false)
		{
			return false;
		}
	}

	time_t timeNow = time(NULL);

	// Cycle through A records
	while (mxRecord.m_addresses.empty() == false)
	{
		ResourceRecord frontRecord(mxRecord.m_addresses.front());
		string hostName(frontRecord.m_hostName);

		mxRecord.m_addresses.pop();

		// Skip discarded records
		if (isDiscarded(frontRecord) == true)
		{
			continue;
		}

		// Is this record still alive ?
		if (frontRecord.hasExpired(timeNow) == true)
		{
			// No, it's not
#ifdef DEBUG
			clog << "SMTPSession::setServer: A record " << hostName
				<< " has expired" << endl;
#endif
			continue;
		}

		// Use port 25 smtp or 587 submission (default) ?
		if (m_domainLimits.m_useSubmissionPort == false)
		{
			hostName += ":25";
		}

		if ((m_session == NULL) ||
			(smtp_set_server(m_session, hostName.c_str()) == 0))
		{
			clog << "Couldn't set server to " << hostName << endl;

			// Give up on this A record
			continue;
		}
		clog << "Set server for " << m_domainLimits.m_domainName << " to " << hostName << endl;

		// Push it back
		mxRecord.m_addresses.push(frontRecord);

		return true;
	}

	return false;
}

void SMTPSession::discardCurrentServer(void)
{
	if (m_topQueue.empty() == true)
	{
		return;
	}

	// Currently-used records were pushed to the back of their respective queues
	ResourceRecord currentMXRecord(m_topQueue.back());
	if (currentMXRecord.m_addresses.empty() == false)
	{
		ResourceRecord &currentARecord = currentMXRecord.m_addresses.back();

		clog << "Discarding " << currentARecord.m_hostName << endl;

		// Discard this particular A record
		m_discarded.insert(currentARecord.m_hostName);
	}
}

bool SMTPSession::isDiscarded(const ResourceRecord &aRecord)
{
	string hostName(aRecord.m_hostName);

	// Was this record previously discarded for some reason ?
	if (m_discarded.find(hostName) != m_discarded.end())
	{
		// Yes, it was
#ifdef DEBUG
		clog << "SMTPSession::isDiscarded: A record " << hostName
			<< " was discarded" << endl;
#endif
		return true;
	}

	return false;
}

void SMTPSession::signMessage(SMTPMessage *pMsg, DomainAuth &domainAuth)
{
	string fullMessage, signatureHeader;

	if ((pMsg == NULL) ||
		(domainAuth.canSign() == false))
	{
		return;
	}

	fullMessage = getMessageData(pMsg);
 
	// Sign the message
	if (domainAuth.sign(fullMessage, pMsg->m_headersList, false,
		signatureHeader) == true)
	{
#ifdef DEBUG
		clog << "SMTPSession::signMessage: signature is '" << signatureHeader << "'" << endl;
#endif

		// Add this as the top header
		signatureHeader.append(pMsg->m_headers);
		pMsg->m_headers = signatureHeader;
	}
}

string SMTPSession::getDomainName(void) const
{
	return m_domainLimits.m_domainName;
}

unsigned int SMTPSession::getTopMXServersCount(void) const
{
	return m_topQueue.size();
}

bool SMTPSession::cycleServers(void)
{
	if (m_topQueue.empty() == true)
	{
		// No more records in this priority, queue the next bunch
		if (queueNextPriorityRecords() == false)
		{
			// We have run out of MX records to try...
			// Get them all again
			if (initializeTopQueue() == false)
			{
				return false;
			}
		}
	}

	time_t timeNow = time(NULL);

	// Cycle through MX records
	while (m_topQueue.empty() == false)
	{
		ResourceRecord frontRecord(m_topQueue.front());

		m_topQueue.pop();

		// Is this record still alive ? Set it as server
		if ((frontRecord.hasExpired(timeNow) == false) &&
			(setServer(frontRecord) == true))
		{
			// Push it back in the queue
			m_topQueue.push(frontRecord);

			return true;
		}
		// Either it's expired, or can't be used as server

#ifdef DEBUG
		clog << "SMTPSession::cycleServers: MX record " << frontRecord.m_hostName
			<< " has expired or has no usable address" << endl;
#endif
		if (m_topQueue.empty() == true)
		{
			// Try the next priority
			if (queueNextPriorityRecords() == false)
			{
				// ...or get all MX records again
				if (initializeTopQueue() == false)
				{
					break;
				}
			}
		}
	}

	return false;
}

bool SMTPSession::generateMessages(DomainAuth &domainAuth,
	MessageDetails *pDetails, map<string, Recipient> &destinations,
	StatusUpdater *pUpdater)
{
	map<string, string> fieldValues;
	set<SMTPMessage *> messages;
	bool messageOk = true;

	if (pDetails == NULL)
	{
		return false; 
	}

	if (destinations.empty() == true)
	{
		return true;
	}
	clog << destinations.size() << " destinations on domain " << m_domainLimits.m_domainName << endl;

	// Can we send the same message to all recipients ?
	if (pDetails->isRecipientPersonalized() == false)
	{
		// Yes, we can !
		map<string, Recipient>::iterator destIter = destinations.begin();
		if (destIter != destinations.end())
		{
			// Get non-recipient related (hopefully) custom fields from the first recipient
			for (map<unsigned int, string>::iterator customIter = destIter->second.m_customFields.begin();
				customIter != destIter->second.m_customFields.end(); ++customIter)
			{
				stringstream fieldName;

				fieldName << "{customfield" << customIter->first << "}";
				fieldValues[fieldName.str()] = customIter->second;
			}
		}

		SMTPMessage *pMessage = new SMTPMessage(fieldValues, pDetails, Notify_NOTSET, false);
		messages.insert(pMessage);

		// Queue the message
		if (queueMessage(pMessage, domainAuth,
			destinations, pUpdater) == false)
		{
			messageOk = false;
		}
	}
	else
	{
		// Each destination requires its own message unfortunately
		for (map<string, Recipient>::iterator destIter = destinations.begin();
			destIter != destinations.end(); ++destIter)
		{
			map<string, Recipient> recipients;
			string name(destIter->second.m_name);
			string emailAddress(destIter->second.m_emailAddress);

			// Fields should be substituted with these values
			fieldValues["{Name}"] = name;
			fieldValues["{emailaddress}"] = emailAddress;
			fieldValues["{recipientId}"] = MessageDetails::encodeRecipientId(destIter->second.m_id);
			for (map<unsigned int, string>::iterator customIter = destIter->second.m_customFields.begin();
				customIter != destIter->second.m_customFields.end(); ++customIter)
			{
				stringstream fieldName;

				fieldName << "{customfield" << customIter->first << "}";
				fieldValues[fieldName.str()] = customIter->second;
#ifdef DEBUG
				clog << "SMTPSession::generateMessages: field " << customIter->first << "=" << customIter->second << endl;
#endif
			}

			recipients[emailAddress] = destIter->second;

			SMTPMessage *pMessage = new SMTPMessage(fieldValues, pDetails, Notify_NOTSET, false);
			messages.insert(pMessage);

			// Queue the message
			// FIXME: it might not be a good idea to queue too many messages, especially if content is huge
			if (queueMessage(pMessage, domainAuth,
				recipients, pUpdater) == false)
			{
				messageOk = false;
			}
		}
	}
	clog << "Queued " << messages.size() << " messages for domain " << m_domainLimits.m_domainName << endl;

	// Force a send
	if (dispatchMessages(pUpdater, true) == false)
	{
		messageOk = false;
	}

	// Delete all messages
	for (set<SMTPMessage *>::const_iterator msgIter = messages.begin();
		msgIter != messages.end(); ++msgIter)
	{
#ifdef DEBUG
		if ((messageOk == false) &&
			(m_errorNum == SMTP_ERR_INVAL))
		{
			SMTPMessage *pMsg = (*msgIter);

			clog << "SMTPSession::generateMessages: API failed on below message" << endl
				<< dumpMessage(pMsg, "", "") << endl;
		}
#endif
		delete (*msgIter);
	}

	return messageOk;
}

bool SMTPSession::queueMessage(SMTPMessage *pMsg,
	DomainAuth &domainAuth, map<string, Recipient> &recipients,
	StatusUpdater *pUpdater)
{
	string defaultReturnPath;
	bool setRecipientSpecificHeaders = false;
	bool setReturnPathHeader = false;
	bool serverOk = true;

	// Create, if necessary
	if (createSession() == false)
	{
		return false;
	}

	if ((m_session == NULL) ||
		(pMsg == NULL))
	{
		return false;
	}

	smtp_message_t message = smtp_add_message(m_session);

	smtp_set_messagecb(message, messageDataCallback, pMsg);

	// Enable Message Disposition Notification ?
	if (pMsg->m_enableMdn == true)
	{
		// Request MDN to be sent to the same address as the reverse path
		smtp_set_header(message, "Disposition-Notification-To", NULL, NULL);
	}

	// Default Return-Path header
	if (pMsg->getSenderEmailAddress().empty() == false)
	{
		defaultReturnPath = pMsg->getSenderEmailAddress();
	}
	else if (pMsg->getFromEmailAddress().empty() == false)
	{
		defaultReturnPath = pMsg->getFromEmailAddress();
	}
	if (defaultReturnPath.empty() == false)
	{
		smtp_set_reverse_path(message, defaultReturnPath.c_str());
#ifdef DEBUG
		clog << "SMTPSession::queueMessage: set default Return-Path to "
			<< defaultReturnPath << endl;
#endif
	}

	// If only one recipient, set To and Return-Path here
	if (((pMsg->m_pDetails != NULL) && (pMsg->m_pDetails->m_to.empty() == true)) &&
		(recipients.size() == 1))
	{
		setRecipientSpecificHeaders = true;
		setReturnPathHeader = true;
	}

	map<string, Recipient>::const_iterator recipIter = recipients.begin();
	while (recipIter != recipients.end())
	{
		string toHeader;
		string ccHeader;

		if (pMsg->m_pDetails != NULL)
		{
			toHeader = pMsg->m_pDetails->m_to;
			ccHeader = pMsg->m_pDetails->m_cc;
		}

		// Add as many recipients as allowed
		while ((m_msgsCount < m_domainLimits.m_maxMsgsPerServer) && (recipIter != recipients.end()))
		{
			string name(recipIter->second.m_name), emailAddress(recipIter->second.m_emailAddress);

			if (setRecipientSpecificHeaders == true)
			{
				toHeader = appendValueAndPath(name, emailAddress);

				setRecipientSpecificHeaders = false;
			}

			if (setReturnPathHeader == true)
			{
				// Return-Path header
				if (recipIter->second.m_returnPathEmailAddress.empty() == false)
				{
					smtp_set_reverse_path(message, recipIter->second.m_returnPathEmailAddress.c_str());
#ifdef DEBUG
					clog << "SMTPSession::queueMessage: overrode Return-Path to "
						<< recipIter->second.m_returnPathEmailAddress << endl;
#endif
				}
				else if (defaultReturnPath.empty() == false)
				{
					smtp_set_reverse_path(message, defaultReturnPath.c_str());
#ifdef DEBUG
					clog << "SMTPSession::queueMessage: reverted to default Return-Path" << endl;
#endif
				}

				setReturnPathHeader = false;
			}

			if ((pMsg->m_pDetails != NULL) &&
				(pMsg->m_pDetails->m_unsubscribeLink.empty() == false))
			{
				string encodedId(MessageDetails::encodeRecipientId(recipIter->second.m_id));
				string::size_type eqPos = encodedId.find('=');

				// Base64 encoding may bring in = characters
				while (eqPos != string::npos)
				{
					encodedId.erase(eqPos, 1);

					if (eqPos >= encodedId.length())
					{
						break;
					}
					eqPos = encodedId.find('=', eqPos);
				}
				pMsg->appendHeader("List-Unsubscribe", pMsg->m_pDetails->m_unsubscribeLink + encodedId, "");
			}

			smtp_recipient_t recipient = smtp_add_recipient(message, emailAddress.c_str());

			// Is Delivery Status Notification required ?
			if (pMsg->m_dsnFlags != Notify_NOTSET)
			{
				smtp_dsn_set_notify(recipient, pMsg->m_dsnFlags);
			}

			++recipIter;
			++m_msgsCount;
		}

		// Set a CC only if there's a To
		// If there's no To, use CC
		if (toHeader.empty() == false)
		{
			pMsg->appendHeader("To", toHeader, "");
			if (ccHeader.empty() == false)
			{
				pMsg->appendHeader("CC", ccHeader, "");
			}
		}
		else if (ccHeader.empty() == false)
		{
			pMsg->appendHeader("To", ccHeader, "");
		}

		// Sign the message
		signMessage(pMsg, domainAuth);

		if (dispatchMessages(pUpdater, false) == false)
		{
			// Chances are this message wasn't delivered to anyone
			serverOk = false;
			break;
		}
	}

	return serverOk;
}

bool SMTPSession::dispatchMessages(StatusUpdater *pUpdater, bool force)
{
	bool serverOk = true;

	if (m_msgsCount == 0)
	{
		// No message queued
		return true;
	}

	// Do we have to send messages now ?
	if ((m_msgsCount < m_domainLimits.m_maxMsgsPerServer) &&
		(force == false))
	{
		// No, we don't
#ifdef DEBUG
		clog << "SMTPSession::dispatchMessages: post-poned sending ("
			<< m_msgsCount << "/" << m_domainLimits.m_maxMsgsPerServer << ")" << endl;
#endif
		return true;
	}

	// This should only be true in test mode
	if (m_dontSend == true)
	{
		// Pretend messages were sent out
		m_msgsCount = 0;
		return true;
	}

	// Reset error variables
	recordError(true);

#ifdef DEBUG
	clog << "SMTPSession::dispatchMessages: sending " << m_msgsCount << " messages" << endl;
#endif
	Timer sessionTimer;
	if (m_mutexSessions == true)
	{
		pthread_mutex_lock(&m_mutex);
	}
	if (!smtp_start_session(m_session))
	{
		recordError();
		serverOk = false;
	}
	// Did some kind of connection error occur ?
	// FIXME: this is hacky
	if ((m_errorNum == SMTP_ERR_DROPPED_CONNECTION) ||
		(m_errorNum == SMTP_ERR_EAI_NONAME) ||
		(m_errorMsg.find("onnect") != string::npos))
	{
		// Try again with another server
		discardCurrentServer();

		serverOk = cycleServers();
		if (serverOk == true)
		{
			smtp_enumerate_messages(m_session, resetMessagesCallback, NULL);

			if (!smtp_start_session(m_session))
			{
				recordError();
			}
			else
			{
				// This time was okay
				recordError(true);
			}
		}
	}
	if (m_mutexSessions == true)
	{
		pthread_mutex_unlock(&m_mutex);
	}

	if (serverOk == true)
	{
		clog << "Sent " << m_msgsCount << " messages to " << m_domainLimits.m_domainName
			<< " in " << sessionTimer.stop() / 1000 << " seconds" << endl;

		// Check all messages for this session
		smtp_enumerate_messages(m_session, messageStatusCallback, pUpdater);
	}
	m_msgsCount = 0;

	// Destroy the current session
	destroySession();
	// ...and create a new one for the next batch
	if (createSession() == false)
	{
		serverOk = false;
	}

	return serverOk;
}

string SMTPSession::dumpMessage(SMTPMessage *pMsg,
	const string &dummyName, const string &dummyEmailAddress)
{
	string defaultReturnPath;

	if (pMsg == NULL)
	{
		return "";
	}

	// To header
	if (dummyEmailAddress.empty() == false)
	{
		pMsg->appendHeader("To", dummyName.c_str(), dummyEmailAddress.c_str());
	}

	return getMessageData(pMsg);
}

void SMTPSession::setError(int errNum)
{
	char errBuf[512];

	m_errorNum = errNum;
	char *pErrorString = smtp_strerror(m_errorNum, errBuf, 512);
	if (pErrorString != NULL)
	{
		m_errorMsg = pErrorString;
	}
}

void SMTPSession::recordError(bool reset)
{
	m_errorMsg.clear();
	if (reset == true)
	{
		m_errorNum = -1;
		return;
	}

	setError(smtp_errno());
	clog << "SMTP error " << m_errorNum << ": " << m_errorMsg << endl;
}

int SMTPSession::getError(string &msg) const
{
	msg = m_errorMsg;

	return m_errorNum;
}

